#include "HelloWindow.h"
#include <QExposeEvent>
#include <QResizeEvent>
#include <QKeyEvent>
#include <QWheelEvent>
#include <QCoreApplication>
#include <QOpenGLVertexArrayObject>
#include <QTime>
#include <QImage>
#include <QMatrix>
#include <QtMath>
#include "Camera.h"

HelloWindow::HelloWindow()
    : QWindow(),
      m_context(nullptr),
      m_update_pending(false),
      m_cubeVAO(nullptr),
      m_lightVAO(nullptr),
      m_vbo(nullptr),
      m_cam(nullptr),
      m_lightPos(1.2f, 1.0f, 2.0f)
{
    setSurfaceType(QWindow::OpenGLSurface);
    resize(800, 600);
    setTitle("LearnOpenGL");
}

HelloWindow::~HelloWindow()
{
    delete m_vbo;
    delete m_cam;
}

void HelloWindow::renderNow()
{
    if (!isExposed())
        return;

    if (!m_context) {
        m_context = new QOpenGLContext(this);
        m_context->setFormat(requestedFormat());
        m_context->create();

        m_context->makeCurrent(this);

        initializeOpenGLFunctions();

        glEnable(GL_DEPTH_TEST);
        QCursor::setPos(geometry().center());

        bool result;
        m_lightProgram = new QOpenGLShaderProgram(this);
        result = m_lightProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, "shader.vert");
        if (!result) { qDebug("Vertex error: %s", qPrintable(m_lightProgram->log())); }
        result = m_lightProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, "shader.frag");
        if (!result) { qDebug("Fragment error: %s", qPrintable(m_lightProgram->log())); }
        result = m_lightProgram->link();
        if (!result) { qDebug("Link error: %s", qPrintable(m_lightProgram->log())); }

        m_lampProgram = new QOpenGLShaderProgram(this);
        result = m_lampProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, "lamp.vert");
        if (!result) { qDebug("Vertex error: %s", qPrintable(m_lampProgram->log())); }
        result = m_lampProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, "lamp.frag");
        if (!result) { qDebug("Fragment error: %s", qPrintable(m_lampProgram->log())); }
        result = m_lampProgram->link();
        if (!result) { qDebug("Link error: %s", qPrintable(m_lampProgram->log())); }


        float vertices[] = {
            -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
             0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
             0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
             0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
            -0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
            -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,

            -0.5f, -0.5f,  0.5f,  0.0f,  0.0f,  1.0f,
             0.5f, -0.5f,  0.5f,  0.0f,  0.0f,  1.0f,
             0.5f,  0.5f,  0.5f,  0.0f,  0.0f,  1.0f,
             0.5f,  0.5f,  0.5f,  0.0f,  0.0f,  1.0f,
            -0.5f,  0.5f,  0.5f,  0.0f,  0.0f,  1.0f,
            -0.5f, -0.5f,  0.5f,  0.0f,  0.0f,  1.0f,

            -0.5f,  0.5f,  0.5f, -1.0f,  0.0f,  0.0f,
            -0.5f,  0.5f, -0.5f, -1.0f,  0.0f,  0.0f,
            -0.5f, -0.5f, -0.5f, -1.0f,  0.0f,  0.0f,
            -0.5f, -0.5f, -0.5f, -1.0f,  0.0f,  0.0f,
            -0.5f, -0.5f,  0.5f, -1.0f,  0.0f,  0.0f,
            -0.5f,  0.5f,  0.5f, -1.0f,  0.0f,  0.0f,

             0.5f,  0.5f,  0.5f,  1.0f,  0.0f,  0.0f,
             0.5f,  0.5f, -0.5f,  1.0f,  0.0f,  0.0f,
             0.5f, -0.5f, -0.5f,  1.0f,  0.0f,  0.0f,
             0.5f, -0.5f, -0.5f,  1.0f,  0.0f,  0.0f,
             0.5f, -0.5f,  0.5f,  1.0f,  0.0f,  0.0f,
             0.5f,  0.5f,  0.5f,  1.0f,  0.0f,  0.0f,

            -0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,
             0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,
             0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,
             0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,
            -0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,
            -0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,

            -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,
             0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,
             0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,
             0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,
            -0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,
            -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f
        };
        m_cubeVAO = new QOpenGLVertexArrayObject(this);
        m_vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);

        m_cubeVAO->create();
        m_vbo->create();

        m_cubeVAO->bind();

        m_vbo->bind();
        m_vbo->setUsagePattern(QOpenGLBuffer::StaticDraw);
        m_vbo->allocate(vertices, sizeof(vertices));

        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (GLvoid *)nullptr);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (GLvoid *)(3*sizeof(GLfloat)));
        glEnableVertexAttribArray(1);


        m_vbo->release();
        m_cubeVAO->release();


        m_lightVAO = new QOpenGLVertexArrayObject(this);
        m_lightVAO->create();
        m_lightVAO->bind();
        m_vbo->bind();
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (GLvoid *)nullptr);
        glEnableVertexAttribArray(0);
        m_vbo->release();
        m_lightVAO->release();


        auto retinaScale = devicePixelRatio();
        glViewport(0, 0,
                   width() * retinaScale,
                   height() * retinaScale);

        m_cam = new Camera();
    }
    else {
        m_context->makeCurrent(this);
    }


    //render
    glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


    QMatrix4x4 view = m_cam->lookAt();

    QMatrix4x4 projection;
    projection.perspective(m_cam->fieldOfView(),
                           static_cast<float>(width())/static_cast<float>(height()),
                           0.1f, 100.0f);
    {
        m_lightProgram->bind();
        m_cubeVAO->bind();

        m_lightProgram->setUniformValue("model", QMatrix4x4());
        m_lightProgram->setUniformValue("view", view);
        m_lightProgram->setUniformValue("projection", projection);

        m_lightProgram->setUniformValue("light.position", m_lightPos);
        double time = QTime::currentTime().msecsTo(QTime(0, 0)) / 1000.0;
        QVector3D lightColor(static_cast<float>(qSin(time*2.0)),
                             static_cast<float>(qSin(time*0.7)),
                             static_cast<float>(qSin(time*1.3)));
        QVector3D diffuseColor = lightColor * 0.5f;
        QVector3D ambientColor = diffuseColor * 0.2f;
        m_lightProgram->setUniformValue("light.ambient", ambientColor);
        m_lightProgram->setUniformValue("light.diffuse", diffuseColor);
        m_lightProgram->setUniformValue("light.specular", QVector3D(1.0f, 1.0f, 1.0f));

        m_lightProgram->setUniformValue("material.ambient", QVector3D(1.0f, 0.5f, 0.31f));
        m_lightProgram->setUniformValue("material.diffuse", QVector3D(1.0f, 0.5f, 0.31f));
        m_lightProgram->setUniformValue("material.specular", QVector3D(0.5f, 0.5f, 0.5f));
        m_lightProgram->setUniformValue("material.shininess", 32.0f);

        m_lightProgram->setUniformValue("viewPos", m_cam->pos());

        glDrawArrays(GL_TRIANGLES, 0, 36);

        m_cubeVAO->release();
        m_lightProgram->release();
    }


    {
        m_lampProgram->bind();
        m_lightVAO->bind();

        QMatrix4x4 model;
        model.translate(m_lightPos);
        model.scale(QVector3D(0.2f, 0.2f, 0.2f));
        m_lampProgram->setUniformValue("model", model);
        m_lampProgram->setUniformValue("view", view);
        m_lampProgram->setUniformValue("projection", projection);

        glDrawArrays(GL_TRIANGLES, 0, 36);

        m_lightVAO->release();
        m_lampProgram->release();
    }



    m_context->swapBuffers(this);
    renderLater();
}

void HelloWindow::renderLater()
{
    if (!m_update_pending) {
        m_update_pending = true;
        QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest));
    }
}

bool HelloWindow::event(QEvent *event) {
    switch (event->type()) {
    case QEvent::UpdateRequest:
        m_update_pending = false;
        renderNow();
        return true;
    default:
        return QWindow::event(event);
    }
}

void HelloWindow::exposeEvent(QExposeEvent *event)
{
    renderNow();
    return QWindow::exposeEvent(event);
}

void HelloWindow::resizeEvent(QResizeEvent *event)
{
    const auto &size = event->size();
    auto retinaScale = devicePixelRatio();
    if (m_context)
        glViewport(0, 0,
                   size.width() * retinaScale,
                   size.height() * retinaScale);
    renderNow();

    return QWindow::resizeEvent(event);
}

void HelloWindow::keyPressEvent(QKeyEvent *event)
{
    switch(auto pressedKey = event->key()) {
    case Qt::Key_A: case Qt::Key_Left:
    case Qt::Key_W: case Qt::Key_Up:
    case Qt::Key_D: case Qt::Key_Right:
    case Qt::Key_S: case Qt::Key_Down:
        if (m_cam) m_cam->onKeyPressed(pressedKey);
        break;
    case Qt::Key_Escape: close(); return;
    default: return QWindow::keyPressEvent(event);
    }
}

void HelloWindow::keyReleaseEvent(QKeyEvent *event)
{
    switch(auto releasedKey = event->key()) {
    case Qt::Key_A: case Qt::Key_Left:
    case Qt::Key_W: case Qt::Key_Up:
    case Qt::Key_D: case Qt::Key_Right:
    case Qt::Key_S: case Qt::Key_Down:
        if (m_cam) m_cam->onKeyReleased(releasedKey);
        break;
    default: return QWindow::keyPressEvent(event);
    }
}

void HelloWindow::mouseMoveEvent(QMouseEvent *event)
{
    if (m_cam) m_cam->onMouseMoved(QCursor::pos() - geometry().center());
    QCursor::setPos(geometry().center());

    return QWindow::mouseMoveEvent(event);
}

void HelloWindow::wheelEvent(QWheelEvent *event)
{
    if (m_cam) m_cam->onMouseWheelRotated(event->angleDelta());
    return QWindow::wheelEvent(event);
}
